Skip to content

feat: support compose secrets via podman secret store#30

Merged
lesnik512 merged 8 commits into
mainfrom
feat/secrets
Jul 10, 2026
Merged

feat: support compose secrets via podman secret store#30
lesnik512 merged 8 commits into
mainfrom
feat/secrets

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Summary

Add compose secrets support — the first Bucket B feature (real translation, not a flag pass-through). Top-level file:/environment: secret definitions become pod-namespaced podman secret create calls; service references become podman run --secret mounts at /run/secrets/<target>; the store is torn down in the existing EXIT trap. external: true is rejected (honest-subset boundary).

A new deep module compose2pod/secrets.py owns the translation: parsing.py gates it, emit.py weaves the store lifecycle in. Import direction is one-way (emit/parsing → secrets → keys/shell/exceptions).

What's supported

  • Top-level secrets: with exactly one file: (host path, resolved against --project-dir, ${VAR} expands at run time) or environment: (piped via printf '%s' "${VAR-}").
  • Service secrets: short form [name] and full long form {source, target, uid, gid, mode} (mode int→octal string, string passthrough).
  • Pod-namespaced store names (<pod>-<name>) isolate concurrent CI runs; trap removes them alongside the pod.
  • Env-source vars flow into the referenced_variables() stderr note.

Security hardening

The whole-branch review found command injection: secret names and env-var names flowed into generated shell raw (they can't be shlex.quoted like values). Closed by charset-validating both at the gate:

  • Secret names must match ^[a-zA-Z0-9][a-zA-Z0-9_.-]*$, env-var names ^[a-zA-Z_][a-zA-Z0-9_]*$ — every admitted char is shell-inert in an unquoted single token.
  • Uses re.fullmatch (an initial re.match admitted a trailing \n, since $ matches before a newline — that reopened injection by splitting the shell line).
  • Long-form uid/gid/mode reject non-scalar/bool; the whole --secret arg is shlex.quoted.
  • Emission branches on source string-ness (matching validation), not file-key presence, so a malformed mixed def is refused loudly rather than crashing raw.

An independent opus re-review confirmed no remaining injection path (charset shell-inert, ASCII classes reject unicode, MULTILINE unset, every emit path gated by validate).

Testing

just test-ci at 100% coverage (247 tests), just lint-ci clean, just check-planning OK. Validation accept/reject matrix, emission (file/env create lines, --secret opts, mode int/string, pod-namespacing, trap secret rm, referenced vars), injection regressions (metachar, newline, unicode-adjacent), and an emit_script integration.

Docs / planning

  • Design: planning/changes/2026-07-10.02-secrets.md
  • Architecture: Secrets section added to architecture/supported-subset.md

validate_secrets picks the source by string-ness (isinstance(def.get(k), str)),
but secret_create_lines/secret_referenced_variables branched on "file" in def
(key presence). A def like {file: [x], environment: VAR} validated as an env
secret yet crashed raw in the file branch (TypeError in Path), violating the
refuse-loudly contract. Align emission with validation's string-ness check.
@lesnik512 lesnik512 merged commit a378994 into main Jul 10, 2026
6 checks passed
@lesnik512 lesnik512 deleted the feat/secrets branch July 10, 2026 11:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant